package bc.sistema.usuario.seguranca;

import bc.exceptions.ParametrosDeLoginInvalidoException;
import bc.exceptions.RespostaInvalidaException;
import bc.exceptions.SenhaInvalidaException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Classe Login.
 *
 * @author Felipe Miranda Medeiros
 * @version 1.2
 */
public final class Login {

    private static final String ALFABETO = "abcdefghijklmnopqrstuvwxyz";
    private static final String NUMEROS = "0123456789";
    private static final String SIMBOLOS = "$%&*@#!_:~";
    private final String username;
    private String senha;

    /**
     * Construtor de Login.
     *
     * @param username Username do usuário. Uma vez definido não é possível
     * modificar.
     * @param senha Senha do usuário. Pode ser alterada seguindo alguns
     * requisitos de segurança.
     * @throws ParametrosDeLoginInvalidoException Se alguns dos parametros
     * passado forem vazios ou nulos.
     */
    public Login(final String username, final String senha) throws ParametrosDeLoginInvalidoException {
        if (username == null) {
            throw new ParametrosDeLoginInvalidoException("username null");
        }
        if (username.equals("")) {
            throw new ParametrosDeLoginInvalidoException("username vazio");
        }
        this.username = username;
        if (senha == null) {
            throw new ParametrosDeLoginInvalidoException("Senha null");
        }
        if (senha.equals("")) {
            throw new ParametrosDeLoginInvalidoException("Senha vazia");
        }
        this.senha = senha;
    }

    /**
     * Altera a senha caso as senhas passadas sejam válidas e a senha atual
     * corresponda com a que o usuário forneceu.
     *
     * @param senhaAtual Senha atual do usuário.
     * @param novaSenha Nova senha atual do usuário.
     * @throws SenhaInvalidaException Se alguns dos parametros passado forem
     * vazios ou nulos, ou se a senha atual não corresponder a passada.
     */
    public void setSenha(String senhaAtual, String novaSenha) throws SenhaInvalidaException {
        if (senhaAtual == null || novaSenha == null) {
            throw new SenhaInvalidaException("Senha null");
        }
        if (senhaAtual.equals("") || novaSenha.equals("")) {
            throw new SenhaInvalidaException("Senha vazia");
        }
        if (!senhaAtual.equals(senha)) {
            throw new SenhaInvalidaException("Senha inválida");
        }
        this.senha = novaSenha;
    }

    /**
     * Altera a senha caso a resposta da pergunta de segurança seja igual a que
     * o usuário passou.
     *
     * @param novaSenha Nova senha que compreende os requisitos mínimos de
     * segurança.
     * @param perguntaSecreta PerguntaSecreta pertecente ao usuário.
     * @param resposta Resposta passada pelo usuário.
     * @throws SenhaInvalidaException Se a nova senha passada for vazia ou nula
     * e ter os requisitos mínimos de segurança.
     * @throws RespostaInvalidaException Se a resposta não compreender a
     * exigencia de pelo menos 5 caracteres ou não for igual a resposta
     * fornecida pelo usuário.
     * @throws NullPointerException Se a pergunta secreta for null.
     */
    public void setSenha(String novaSenha, PerguntaSecreta perguntaSecreta,
            String resposta) throws SenhaInvalidaException, RespostaInvalidaException,
            NullPointerException {

        senhaValida(novaSenha);

        if (perguntaSecreta == null) {
            throw new NullPointerException("Pergunta null");
        }

        if (!PerguntaSecreta.respostaValida(resposta) || !perguntaSecreta.validarResposta(resposta)) {
            throw new RespostaInvalidaException("Resposta invalida");
        }

        this.senha = novaSenha;
    }

    /**
     * Verifica se uma senha atende os requistos mínimos de segurança.
     *
     * @param senha Senha a ser verificada.
     * @throws SenhaInvalidaException Se a senha passada for vazia ou nula e ter
     * os requisitos mínimos de segurança.
     */
    public static void senhaValida(String senha) throws SenhaInvalidaException {
        if (senha == null) {
            throw new SenhaInvalidaException("Senha null");
        }
        if (senha.equals("")) {
            throw new SenhaInvalidaException("Senha vazia");
        }
        if (senha.length() < 5) {
            throw new SenhaInvalidaException("Senha curta");
        }
    }

    /**
     * Calcula a pontuação da senha baseado alguns fatores.
     * <ul>
     * <li>Tamanho</li <li>Número de letras maiúsculas</li>
     * <li>Número de letras minúsculas</li>
     * <li>Número de dígitos</li>
     * <li>Número de dígitos e sequencias no meio</li>
     * <li>Caracteres repetidos</li>
     * <li>Sequencias</li>
     *
     * Um bonus é adicionado a senha se ela tiver 8 ou mais caracteres com
     * combinações de letras maiúsculas, minúsculas, digitos e símbolos
     *
     * @param senha A senha à ser checada.
     * @return Uma Enum que descreve a avaliação da senha como FRACA, BOA e
     * FORTE.
     */
    public static ForcaDaSenha forcaDaSenha(final String senha) {
        int pontuacao = 0;

        // Tamanho
        pontuacao += (senha.length() * 4);

        // Letras maiusculas
        final int maiuscula = matches(senha, "[A-Z]");

        if (maiuscula > 0) {
            pontuacao += ((senha.length() - maiuscula) * 2);
        }

        // Letras minusculas
        final int minuscula = matches(senha, "[a-z]");

        if (minuscula > 0) {
            pontuacao += ((senha.length() - minuscula) * 2);
        }

        // Numeros
        final int numeros = matches(senha, "[0-9]");

        if (numeros > 0) {
            pontuacao += (numeros * 4);
        }
        if (numeros == senha.length()) {
            pontuacao -= numeros;
        }

        // Caracteres especiais
        final int caracteresEspeciais = matches(senha, "[:,!,@,#,$,%,^,&,*,?,_,~]");

        if (caracteresEspeciais > 0) {
            pontuacao += (caracteresEspeciais * 6);
        }

        // Somente letras
        final int letras = matches(senha, "[a-z|A-Z]");

        if (letras == senha.length()) {
            pontuacao -= letras;
        }

        // Numeros do meio
        int meios = 0;

        for (int j = 0; j < NUMEROS.length(); ++j) {
            if ((senha.indexOf(NUMEROS.charAt(j)) > 0) && (senha.indexOf(NUMEROS.charAt(j)) < senha.length() - 1)) {
                ++meios;
            }
        }

        // Simbolos do meio
        for (int j = 0; j < SIMBOLOS.length(); ++j) {
            if ((senha.indexOf(SIMBOLOS.charAt(j)) > 0) && (senha.indexOf(SIMBOLOS.charAt(j)) < senha.length())) {
                ++meios;
            }
        }

        pontuacao += (meios * 2);

        // Caracteres repetidos
        final List<Character> analisados = new ArrayList<>();
        int repetidos = 0;

        for (int i = 0; i < senha.length(); ++i) {
            int matches = 0;

            for (int j = 0; j < senha.length(); ++j) {
                if (senha.toLowerCase().charAt(i) == senha.toLowerCase().charAt(j) && !analisados.contains(senha.toLowerCase().charAt(i))) {
                    ++matches;
                }
            }

            if (matches > 1) {
                pontuacao -= (matches * (matches - 1));
                repetidos += (matches * (matches - 1));
                analisados.add(senha.toLowerCase().charAt(i));
            }
        }

        // Letras maiusculas consecutivas
        final int letrasMaiusculasConsecutivas = matches(senha, "[A-Z]{2}");
        pontuacao -= (letrasMaiusculasConsecutivas * 2);

        // Letras minusculas consecutivas
        final int letrasMinusculasConsecutivas = matches(senha, "[a-z]{2}");
        pontuacao -= (letrasMinusculasConsecutivas * 2);

        // Numeros consecutivos
        final int numerosConsecutivos = matches(senha, "[0-9]{2}");
        pontuacao -= (numerosConsecutivos * 2);

        // Caracteres consecutivos
        for (int i = 0; i < senha.length(); ++i) {
            final int index = ALFABETO.indexOf(String.valueOf(senha.charAt(i)).toLowerCase());

            if (index != -1 && index < ALFABETO.length() - 3) {
                final String sequenciaNormal = ALFABETO.substring(index, index + 3);
                final String sequenciaInversa = new StringBuffer(sequenciaNormal).reverse().toString();

                if (senha.indexOf(sequenciaNormal) != -1 || senha.indexOf(sequenciaInversa) != -1) {
                    pontuacao -= 3;
                }
            }
        }

        // Sequencia de numeros
        for (int i = 0; i < senha.length(); ++i) {
            final int index = NUMEROS.indexOf(String.valueOf(senha.charAt(i)).toLowerCase());

            if (index != -1 && index < NUMEROS.length() - 3) {
                final String sequenciaNormal = NUMEROS.substring(index, index + 3);
                final String sequenciaInversa = new StringBuffer(sequenciaNormal).reverse().toString();

                if (senha.indexOf(sequenciaNormal) != -1 || senha.indexOf(sequenciaInversa) != -1) {
                    pontuacao -= 3;
                }
            }
        }

        // Bonus
        int bonus = 0;

        if (senha.length() >= 8) {
            ++bonus;
            if (((double) maiuscula / (double) senha.length()) >= 0.25) {
                ++bonus;
            }
            if (((double) minuscula / (double) senha.length()) >= 0.25) {
                ++bonus;
            }
            if (((double) numeros / (double) senha.length()) >= 0.25) {
                ++bonus;
            }
            if (((double) caracteresEspeciais / (double) senha.length()) >= 0.25) {
                ++bonus;
            }
        }

        pontuacao += (bonus * 2);

        if (pontuacao < 0) {
            pontuacao = 0;
        } else if (pontuacao > 100) {
            pontuacao = 100;
        }

        if (pontuacao < 34) {
            return ForcaDaSenha.FRACA;
        } else if (pontuacao > 34 && pontuacao < 68) {
            return ForcaDaSenha.BOA;
        } else {
            return ForcaDaSenha.FORTE;
        }
    }

    private static int matches(final String string, final String regexPattern) {
        int matches = 0;
        final Pattern pattern = Pattern.compile(regexPattern);
        final Matcher matcher = pattern.matcher(string);

        while (matcher.find()) {
            ++matches;
        }

        return matches;
    }

    /**
     * Verifica se o username e a senha são iguais as criadas pelo usuário.
     *
     * @param username Username a ser validado.
     * @param senha Senha a ser validada.
     * @return True se a senha e o username passados como parametros forem
     * iguais ao que foi criado no momento do cadastro, e False se apenas um
     * deles não forem validados.
     */
    public boolean validar(final String username, final String senha) {
        if ((username == null || senha == null)
                || (!username.equals(this.username) || !senha.equals(this.senha))) {
            return false;
        }
        return true;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || (getClass() != obj.getClass())) {
            return false;
        }
        Login other = (Login) obj;
        if (username == null) {
            if (other.username != null) {
                return false;
            }
        } else if (!username.equals(other.username)) {
            return false;
        }
        return true;
    }
}
